package com.yvan.serverless.mybatis;

import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.google.common.base.Strings;
import com.yvan.Conv;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;

@Slf4j
public class MapperWatcher implements Runnable {

    /**
     * 记录错误
     **/
    private static volatile Map<String, Exception> xmlExceptionMap = new HashMap<>();

    /**
     * 记录当前错误文件
     **/
    private static volatile String errorFilePath = "";

    /**
     * 获取错误
     **/
    public static Exception getXmlException() {
        if (Strings.isNullOrEmpty(errorFilePath)) {
            Object[] keys = xmlExceptionMap.keySet().toArray();
            if (keys != null && keys.length > 0) {
                return xmlExceptionMap.get(Conv.asString(keys[0]));
            }
        }
        return xmlExceptionMap.get(errorFilePath);
    }

    private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
    WatchService watchService = FileSystems.getDefault().newWatchService();
    private Configuration configuration;
    /**
     * 记录jar包存在的mapper
     */
    private final SqlSessionFactory sqlSessionFactory;
    private MybatisPlusProperties mybatisPlusProperties;
    /**
     * 是否开启刷新mapper
     */
    private final boolean enabled;

    /**
     * xml文件目录
     */
    private volatile Map<String, Set<String>> watchFileMap;
    private volatile Map<String, Set<String>> lastWatchFileMap;
    private volatile Map<String, Set<String>> newWatchFileMap;
    private volatile Map<String, Set<String>> deleteWatchFileMap;

    // 标记是否第一次加载
    private volatile Boolean isFirstLoad = true;

    public MapperWatcher(SqlSessionFactory sqlSessionFactory, MybatisPlusProperties mybatisPlusProperties, boolean enabled) throws IOException {
        this.sqlSessionFactory = sqlSessionFactory;
        this.configuration = sqlSessionFactory.getConfiguration();
        this.mybatisPlusProperties = mybatisPlusProperties;
        this.enabled = enabled;
    }

    public void watch() throws IOException, InterruptedException {
        this.run();
    }

    @Override
    public void run() {
        if (!enabled) {
            return;
        }
        final MapperWatcher runnable = this;
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(3 * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                refreshXMLFilePaths();
                /** 处理新增dir **/
                for (String fileDir : newWatchFileMap.keySet()) {
                    Path path = Paths.get(fileDir);
                    try {
                        path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    if (isFirstLoad) {
                        isFirstLoad = false;
                        continue;
                    }
                    for (String filePath : newWatchFileMap.get(fileDir)) {
                        try {
                            xmlExceptionMap.remove(filePath);
                            errorFilePath = "";
                            refresh(new FileSystemResource(new File(filePath)));
                        } catch (Exception e) {
                            e.printStackTrace();
                            Exception ee = new Exception("file:" + filePath + "  " + e.toString(), e);
                            xmlExceptionMap.put(filePath, ee);
                            errorFilePath = filePath;
                        }
                    }
                }
                /** 处理删除dir **/
                for (String fileDir : deleteWatchFileMap.keySet()) {
                    for (String filePath : deleteWatchFileMap.get(fileDir)) {
                        try {
                            xmlExceptionMap.remove(filePath);
                            errorFilePath = "";
                            refresh(new FileSystemResource(new File(filePath)));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "mybatis-plus MapperWatcherww").start();
        new Thread(() -> {
            while (true) {
                //拉取一个WatchKey。当触发监听的事件时，就会产生一个WatchKey，此WatchKey封装了事件信息。
                WatchKey watchKey = null;
                try {
                    watchKey = watchService.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (WatchEvent event : watchKey.pollEvents()) {
                    if (log.isDebugEnabled()) {
                        log.debug(event.context() + "发生了" + event.kind() + "事件！");
                    }
                    if (Conv.asString(event.context()).endsWith(".xml")) {
                        String filePath = watchKey.watchable().toString() + "/" + event.context();
//                        System.out.println(filePath + "发生了" + event.kind() + "事件！");
                        try {
                            xmlExceptionMap.remove(filePath);
                            errorFilePath = "";
                            runnable.refresh(new FileSystemResource(new File(filePath)));
                        } catch (Exception e) {
                            e.printStackTrace();
                            Exception ee = new Exception("file:" + filePath + "  " + e.toString());
                            xmlExceptionMap.put(filePath, ee);
                            errorFilePath = filePath;
                        }
                        if (event.kind().equals(StandardWatchEventKinds.ENTRY_DELETE)) {
                            xmlExceptionMap.remove(filePath);
                            errorFilePath = "";
                        }
                    }
                }
                //虽然是while()循环，但WatchKey和ByteBuffer一样，使用完要重置状态，才能继续用。
                watchKey.reset();    //如果不重置，WatchKey使用一次过后就不能再使用，即只能监听到一次文件变化。
            }
        }, "mybatis-plus MapperWatcher").start();
    }

    private void refreshXMLFilePaths() {
        Resource[] resources = Stream.of(Optional.ofNullable(mybatisPlusProperties.getMapperLocations()).orElse(new String[0]))
                .flatMap(location -> {
                    try {
                        return Stream.of(resourceResolver.getResources(location));
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                .toArray(Resource[]::new);

        if (resources != null) {
            if (watchFileMap == null) {
                watchFileMap = new HashMap<>();
            }
            if (newWatchFileMap == null) {
                newWatchFileMap = new HashMap<>();
            }
            if (deleteWatchFileMap == null) {
                deleteWatchFileMap = new HashMap<>();
            }
            if (lastWatchFileMap == null) {
                lastWatchFileMap = new HashMap<>();
            }
            newWatchFileMap.clear();
            lastWatchFileMap.clear();
            lastWatchFileMap.putAll(watchFileMap);
            deleteWatchFileMap.clear();
            deleteWatchFileMap.putAll(lastWatchFileMap);
            watchFileMap.clear();
            for (Resource mapperLocation : resources) {
                try {
                    String key = "";
                    String dir = "";
                    if (ResourceUtils.isJarURL(mapperLocation.getURL())) {
                        key = new UrlResource(ResourceUtils.extractJarFileURL(mapperLocation.getURL()))
                                .getFile().getPath();
                        dir = new UrlResource(ResourceUtils.extractJarFileURL(mapperLocation.getURL()))
                                .getFile().getParent();
                    } else {
                        key = mapperLocation.getFile().getPath();
                        dir = mapperLocation.getFile().getParent();
                    }
                    if (watchFileMap.keySet().contains(dir)) {
                        watchFileMap.get(dir).add(key);
                    } else {
                        Set<String> tSet = new HashSet<>();
                        tSet.add(key);
                        watchFileMap.put(dir, tSet);
                    }
                    if (!lastWatchFileMap.keySet().contains(dir)) {
                        newWatchFileMap.put(dir, watchFileMap.get(dir));
                    } else {
                        deleteWatchFileMap.remove(dir);
                    }
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            }
            /** 找到 **/

            isFirstLoad = false;
        }
    }


    /**
     * 刷新mapper
     */
    @SuppressWarnings("rawtypes")
    private void refresh(Resource resource) throws NoSuchFieldException, IllegalAccessException {
        this.configuration = sqlSessionFactory.getConfiguration();
        boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
        try {
            Field loadedResourcesField = isSupper ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
                    : configuration.getClass().getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
            XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(),
                    new XMLMapperEntityResolver());
            XNode context = xPathParser.evalNode("/mapper");
            String namespace = context.getStringAttribute("namespace");
            Field field = MapperRegistry.class.getDeclaredField("knownMappers");
            field.setAccessible(true);
            Map mapConfig = (Map) field.get(configuration.getMapperRegistry());
            try {
                mapConfig.remove(Resources.classForName(namespace));
            } catch (ClassNotFoundException e) {
            }
            loadedResourcesSet.remove(resource.toString());
            configuration.getCacheNames().remove(namespace);
            cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace);
            cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace);
            cleanKeyGenerators(context.evalNodes("insert|update"), namespace);
            cleanSqlElement(context.evalNodes("/mapper/sql"), namespace);

            Set<String> needClearSet = new HashSet<>();
            Iterator<MappedStatement> it = sqlSessionFactory.getConfiguration().getMappedStatements().iterator();
            while (it.hasNext()) {
                Object object = it.next();
                if (object instanceof MappedStatement) {
                    MappedStatement statement = (MappedStatement) object;
                    if (resource.toString().equals(statement.getResource())) {
                        needClearSet.add(statement.getId());
                    }
                }
            }
//            sqlSessionFactory.getConfiguration().getMappedStatementNames().forEach((id) -> {
//                MappedStatement statement = sqlSessionFactory.getConfiguration().getMappedStatement(id);
//                if (resource.toString().equals(statement.getResource())) {
//                    // sqlSessionFactory.getConfiguration().getMappedStatements()
//                    needClearSet.add(id);
//                }
//            });
            Field mappedStatementsField = Configuration.class.getDeclaredField("mappedStatements");
            mappedStatementsField.setAccessible(true);
            HashMap<String, Object> mappedStatements = (HashMap<String, Object>) mappedStatementsField.get(sqlSessionFactory.getConfiguration());
            needClearSet.forEach((k) -> mappedStatements.remove(k));

            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
                    sqlSessionFactory.getConfiguration(),
                    resource.toString(), sqlSessionFactory.getConfiguration().getSqlFragments());
            xmlMapperBuilder.parse();
            log.info("refresh: '" + resource + "', success!");

        } catch (Exception e) {
            log.error("Refresh" + resource + " IOException :" + e.getMessage(), e);

        } finally {
            ErrorContext.instance().reset();
        }
    }

    /**
     * 清理parameterMap
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanParameterMap(List<XNode> list, String namespace) {
        for (XNode parameterMapNode : list) {
            String id = parameterMapNode.getStringAttribute("id");
            configuration.getParameterMaps().remove(namespace + StringPool.DOT + id);
        }
    }

    /**
     * 清理resultMap
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanResultMap(List<XNode> list, String namespace) {
        for (XNode resultMapNode : list) {
            String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
            configuration.getResultMapNames().remove(id);
            configuration.getResultMapNames().remove(namespace + StringPool.DOT + id);
            clearResultMap(resultMapNode, namespace);
        }
    }

    private void clearResultMap(XNode xNode, String namespace) {
        for (XNode resultChild : xNode.getChildren()) {
            if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
                    || "case".equals(resultChild.getName())) {
                if (resultChild.getStringAttribute("select") == null) {
                    configuration.getResultMapNames().remove(
                            resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    configuration.getResultMapNames().remove(
                            namespace + StringPool.DOT + resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
                        clearResultMap(resultChild, namespace);
                    }
                }
            }
        }
    }

    /**
     * 清理selectKey
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanKeyGenerators(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
            configuration.getKeyGeneratorNames().remove(namespace + StringPool.DOT + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
        }
    }

    /**
     * 清理sql节点缓存
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanSqlElement(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getSqlFragments().remove(id);
            configuration.getSqlFragments().remove(namespace + StringPool.DOT + id);
        }
    }

}
